test(e2e): real-device Playwright messaging suite#1121
Conversation
…tore addConnectionAndConnect() adds a connection and then connects to it in the same tick, but connect() resolved the id against the memoized `connections` closure, which is stale until the hook re-renders. The just-added id was therefore reported as an unknown connection id and Save silently never connected any HTTP/Serial/Bluetooth device. Read savedConnections from useDeviceStore.getState() so the lookup always sees the live store.
Drives the actual web app in Chromium against real meshtasticd firmware over the HTTP phone API and verifies text messaging in both directions across a two-node mesh. Nodes mesh over the firmware's built-in UDP multicast (224.0.0.69) with no MQTT/relay; distinct node numbers, real encryption. - Default backend: two Docker meshtasticd sim nodes (daily-debian). The same specs run against physical hardware via E2E_DEVICE_MODE=hardware. - An off-browser Python meshtastic peer (e2e/peer/peer.py) drives/asserts the non-browser node over the TCP phone API, mirroring firmware mcp-server tests. - Coverage: connect over HTTPS, mesh->web receive, web->mesh send. Direct messages are fixme'd (see below). CI workflow runs it on Linux. Bugs surfaced by the suite: - Fixed (prior commit): connect-on-save never connected (stale-closure id lookup in useConnections). - Not fixed: apps/web/src/core/subscriptions.ts throws 'ReferenceError: nodeDB is not defined' on every device-metrics telemetry packet (the #1050 migration removed that store); caught per-packet, so messaging still works. - Not fixed: direct messages are blocked by a PKI 'Keys Mismatch' (the SDK's stored peer public key != the key presented during NodeInfo exchange), seen even with fresh sim nodes.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Pull request overview
Adds a Playwright end-to-end suite that drives the real web app (Chromium) against real meshtasticd firmware nodes (Docker sim by default, hardware optional) to validate bidirectional text messaging over the HTTP(S) phone API + a two-node UDP-multicast mesh. Also includes a small but user-impacting fix to connection “connect-on-save” behavior in the web app.
Changes:
- Introduces Playwright E2E infrastructure (config, scripts, CI workflow) and a documented real-device test harness.
- Adds the E2E mesh topology (docker-compose + node configs), a Python TCP “peer” driver, and Playwright page objects + messaging specs.
- Fixes
useConnections.connect()to read the newly-added connection from the live store instead of a stale hook closure.
Reviewed changes
Copilot reviewed 20 out of 21 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
pnpm-lock.yaml |
Adds Playwright dependencies to the lockfile. |
playwright.config.ts |
New Playwright config wiring global setup/teardown, web server, and Chromium project settings. |
package.json |
Adds test:e2e* scripts and @playwright/test devDependency. |
e2e/tests/connect.spec.ts |
E2E “connect + reach messages view” smoke test. |
e2e/tests/messaging.broadcast.spec.ts |
E2E broadcast messaging tests (mesh→web and web→mesh). |
e2e/tests/messaging.direct.spec.ts |
Direct-message spec (currently fixme due to key mismatch behavior). |
e2e/README.md |
Full documentation for running the suite, env vars, and topology details. |
e2e/peer/requirements.txt |
Python peer dependency list (meshtastic). |
e2e/peer/peer.py |
Standalone Python peer for sending/receiving messages and reporting node number via TCP phone API. |
e2e/pages/ConnectionPage.ts |
Page object for driving the “Add Connection” dialog and connecting via HTTP(S). |
e2e/pages/MessagesPage.ts |
Page object for composer interactions and message assertions. |
e2e/fixtures/test.ts |
Shared Playwright fixtures for device info and page objects. |
e2e/fixtures/peer.ts |
TS wrapper around the Python peer process (send/recv/node-num). |
e2e/global-setup.ts |
Brings up mesh topology (docker mode) and waits for device + peer readiness. |
e2e/global-teardown.ts |
Tears down (or leaves running) the docker mesh after tests. |
e2e/device/docker-compose.yml |
Two-node meshtasticd sim topology for the suite. |
e2e/device/nodeA.yaml |
Node A config (HTTPS webserver + UDP multicast enabled). |
e2e/device/nodeB.yaml |
Node B config (UDP multicast enabled, no webserver). |
e2e/.gitignore |
Ignores Playwright artifacts and the Python venv/cache. |
apps/web/src/pages/Connections/useConnections.ts |
Fixes connect-on-save by reading the connection from the live store. |
.github/workflows/e2e.yml |
Adds CI workflow to run the E2E suite and upload the Playwright report. |
Files not reviewed (1)
- pnpm-lock.yaml: Generated file
| const shouldDown = !!process.env.CI || process.env.E2E_DOCKER_DOWN === "1"; | ||
| if (MODE !== "docker") return; | ||
| if (shouldDown) { | ||
| console.log("[e2e] tearing down meshtasticd mesh ..."); | ||
| execSync(`docker compose -f ${COMPOSE_FILE} down -v`, { stdio: "inherit" }); | ||
| } else { | ||
| console.log("[e2e] leaving meshtasticd mesh running (set E2E_DOCKER_DOWN=1 to stop)."); | ||
| } |
- waitForTcp(): destroy the probe socket on the error path so repeated connection failures don't accumulate sockets/FDs across the retry loop. - Don't remove the mesh containers in Playwright globalTeardown in CI — it raced the workflow's failure log capture. Teardown is now gated on E2E_DOCKER_DOWN only; CI dumps device logs on failure and tears the mesh down in a final always() workflow step.
apps/web/src/core/subscriptions.ts called nodeDB.addDeviceMetrics() on every device-metrics telemetry packet, but the #1050 migration removed that store — so it threw 'ReferenceError: nodeDB is not defined' on each telemetry packet (caught per-packet by the SDK's HandleFromRadio, so messaging still worked but the error spammed the console). Route device metrics into the SDK NodesClient via onTelemetryPacket instead — mirroring the existing position handler; the Node domain already carries a deviceMetrics field — and drop the dead app-side handler. Adds a NodesClient test covering the fold.
The direct-message fixme is a simulator limitation, not a web-app bug: the keyless meshtasticd sim nodes NAK a DM with NO_CHANNEL (routing error 6) — no Curve25519 keypair is provisioned/shared, and current firmware can't deliver a direct message without a per-node key / decryptable channel. The app surfaces this correctly (key-refresh dialog). Re-enable against hardware or once the sim provisions keys. Also: mark the nodeDB telemetry bug fixed and note the CI teardown change.
|
Addressed the review feedback + the two bugs the suite surfaced: Copilot review
Bug 1 — Bug 2 — direct messages (determined to be a simulator limitation, not a web bug) Suite still green: 3 passed, 1 skipped. |
Followed up on the suggestion to provision keys in config.security: the keys
ARE settable and persist (verified via admin), but on the native meshtasticd
sim they don't sync to the node's owner / NodeInfo key — owner.public_key stays
empty and the node keeps its MAC-derived num — so the two nodes never exchange
keys. Combined with the firmware refusing non-PKI DMs ('Unknown public key for
destination ... refusing to send legacy DM'), the DM is NAK'd with NO_CHANNEL.
A firmware/sim limitation; DMs work on real hardware. Spec stays fixme.
Per the steer to research the firmware: PKI keygen is gated on a set LoRa
region (NodeDB.cpp:3051) and the sim boots region-UNSET — setting lora.region
via admin DOES make the nodes generate and exchange keys (verified both ways).
But a PKI-encrypted DM still can't traverse the SimRadio: the PKC overhead
exceeds its payload limit ('Payload size larger than compressed message allows!
Send empty payload'), so the packet is truncated and the receiver NAKs
NO_CHANNEL ('No suitable channel found for decoding, hash 0x0'). The firmware
skips PKC under --sim (Router.cpp:730) for exactly this reason, but --sim also
disables the config-file loading the web app needs, so they're mutually
exclusive. DMs work on real hardware; spec stays fixme with this detail.
|
Update on the DM
Net: DMs can't traverse the simulated radio in the config the web app requires; they work on real hardware (real LoRa carries PKC). DM spec stays |
|
@thebentern small nitpick, can details about this get added to the readme.md in the apps/web folder? I think it's important usrre if this repo understand how this testing works |
What
A Playwright end-to-end suite that drives the real web app in Chromium
against real
meshtasticdfirmware over the HTTP phone API and verifiestext messaging in both directions across a real two-node mesh.
How it works
Two
meshtasticdsim nodes (Docker) mesh over the firmware's built-in UDPmulticast LAN transport (
224.0.0.69:4403) — real firmware, real encryption,distinct node numbers, no MQTT / no relay. The browser connects to Node A's
HTTPS phone API; an off-browser Python
meshtasticpeer drives/assertsNode B over the TCP phone API. The same specs run against physical hardware
via
E2E_DEVICE_MODE=hardware(the radio is the bridge).Builds on the firmware's own test foundation (the
meshtasticdsimulator andthe
mcp-server/tests/meshReceiveCollector/wait_forpatterns).Run it
CI:
.github/workflows/e2e.yml(Linux, Docker sim). Full docs + env vars +hardware mode:
e2e/README.md.🐞 Bugs surfaced by the suite
The suite immediately caught regressions from the #1050 SDK migration on
main:useConnections.connect()resolved the connection id against a stale memoized
connectionsclosure, soaddConnectionAndConnect()reportedunknown connection idand Save neveractually connected any HTTP/Serial/Bluetooth device. Now reads from the
live store (
useDeviceStore.getState()). This affects real users, not justthe test.
ReferenceError: nodeDB is not definedinapps/web/src/core/subscriptions.tson every device-metrics telemetry packet(the migration removed that store). Caught per-packet so messaging still
works, but it logs an error each time. The proper fix is a design call: route
device metrics into the SDK
NodesClientvs. drop the handler.SDK's stored public key for the peer node does not match the key presented
during NodeInfo exchange, even with fresh sim nodes. The DM test is
fixmepending key-verification work.
Known limitations / notes
fixme(see Wrong Time-stamp in firmware 1.2.50 #3); broadcast already covers bidirectional messaging.sqlocal(SQLite/OPFS) store times out in headless Chromium andfalls back to in-memory;
MessagesPage.waitReady()gates on the "Connected"status so a send issued the instant the composer renders isn't silently
dropped.
meshtastic/meshtasticd:daily-debian— the:latesttag (2.7.15)predates the
EnableUDPmulticast feature, so sim nodes wouldn't mesh.